Skip to content

Add ForEntityIds for fan-out enrichment (1-event-to-N-entities)#208

Merged
jeremydmiller merged 1 commit intomainfrom
add-for-entity-ids
May 7, 2026
Merged

Add ForEntityIds for fan-out enrichment (1-event-to-N-entities)#208
jeremydmiller merged 1 commit intomainfrom
add-for-entity-ids

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Summary

Companion API addition for JasperFx/marten#4329. Closes the 1-to-N gap reported in @xander-abn's followup — the existing declarative enrichment chain only models a single referenced entity per event, so a projection that needs to enrich one event with several upstream entities of the same type (e.g. an event carrying a list of foreign-key ids) had to drop down to `EnrichUsingEntityQuery` and write the fan-out by hand.

This adds a first-class declarative variant that mirrors the existing 1-to-1 chain:

```csharp
await group
.EnrichWith()
.ForEvent()
.ForEntityIds(e => e.ProviderIds) // Func<TEvent, IEnumerable>
.AddReferences(); // one References per resolved id
```

What changes

  • `EventStep<TEntity, TEvent>.ForEntityIds(...)` — selector returns `IEnumerable`. Returns the new `MultiIdentityStep<TEntity, TEvent, TEntityId>`.
  • `ForEntityIdsFromEvent` — companion overload that hands the `IEvent` wrapper to the selector (matches the existing `ForEntityIdFromEvent` shape so callers can also reach headers, sequence, etc.).
  • `MultiIdentityStep` — mirrors `IdentityStep`: `AddReferences`, `EnrichAsync`, and `FetchEntitiesAsync`. `AddReferences` emits one `References` per resolved id in order; `EnrichAsync` invokes the callback once per (slice, event, resolved entity) triple. Missing ids are silently skipped (consistent with how `IdentityStep` handles a non-resolving single id).
  • `FetchEntitiesAsync` de-duplicates ids before `LoadManyAsync` so the storage layer is never asked for the same id twice within an enrichment.

Test plan

5 new unit tests in `SliceGroupTests`:

  • `for_entity_ids_fans_out_one_event_to_many_references` — happy path
  • `for_entity_ids_skips_missing_ids` — missing ids don't materialize as null references
  • `for_entity_ids_dedupes_loads_for_same_id_across_events` — storage-layer dedupe
  • `for_entity_ids_returns_nullo_cache_when_no_ids_extracted` — empty fast path
  • `for_entity_ids_enrich_async_invokes_callback_per_resolved_entity` — callback semantics

All 240 `EventTests` pass on net8.0 / net9.0 / net10.0.

Pairs with #206

The cache-eviction correctness fix in JasperFx/jasperfx#206 (just merged) is the prerequisite for using `ForEntityIds` in composite projections at scale — without that fix, a single event referencing more upstream entities than `CacheLimitPerTenant` holds would silently drop the evicted ones onto `LoadManyAsync`, which can't see in-flight upstream writes. Both will ship together in the next `JasperFx.Events` release.

🤖 Generated with Claude Code

…to-N-entities)

Closes the 1-to-N gap that fell out of JasperFx/marten#4329 — the existing
EnrichWith<TEntity>().ForEvent<TEvent>().ForEntityId(selector).AddReferences()
chain only models a single referenced entity per event. Real-world projections
often need to enrich one event with several upstream entities of the same type
(e.g. an event carrying a list of foreign-key ids).

Until now the only supported workaround was to drop down to the
EnrichUsingEntityQuery escape hatch and write the fan-out by hand. This adds
a first-class, declarative variant that mirrors the existing 1-to-1 chain:

    await group
        .EnrichWith<Provider>()
        .ForEvent<ShiftScheduled>()
        .ForEntityIds(e => e.ProviderIds)   // Func<TEvent, IEnumerable<TEntityId>>
        .AddReferences();                    // one References<Provider> per resolved id

Implementation:

- New EventStep<TEntity, TEvent>.ForEntityIds<TEntityId>(...) returning
  MultiIdentityStep<TEntity, TEvent, TEntityId>.
- Companion ForEntityIdsFromEvent that hands the IEvent<TEvent> wrapper to
  the selector, matching the pattern of the existing ForEntityIdFromEvent.
- MultiIdentityStep mirrors IdentityStep: AddReferences, EnrichAsync, and
  FetchEntitiesAsync. AddReferences emits one References<TEntity> per resolved
  id in order; EnrichAsync invokes the callback once per (slice, event,
  resolved entity) triple. Missing ids are silently skipped.
- FetchEntitiesAsync de-duplicates ids before LoadManyAsync so the storage
  layer is never asked for the same id twice within an enrichment.

Tests cover: per-event fan-out via AddReferences, missing-id skipping,
storage-side dedupe across events that reference the same id, the empty-ids
fast path, and EnrichAsync's per-resolved-entity callback semantics.

Refs JasperFx/marten#4329.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jeremydmiller jeremydmiller merged commit 50af23a into main May 7, 2026
1 check passed
@jeremydmiller jeremydmiller deleted the add-for-entity-ids branch May 7, 2026 13:59
jeremydmiller added a commit that referenced this pull request May 7, 2026
Two changes ride this release:

1. Composite-projection cache eviction fix (#206): downstream stages of a
   composite no longer lose access to upstream in-flight entities when
   CacheLimitPerTenant is small relative to per-batch fan-out.

2. ForEntityIds (#208): first-class 1-event-to-N-entities enrichment via
   group.EnrichWith<T>().ForEvent<E>().ForEntityIds(selector).AddReferences().

Refs JasperFx/marten#4329, #206, #208.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jeremydmiller added a commit to JasperFx/marten that referenced this pull request May 7, 2026
JasperFx.Events 1.35.0 ships two changes that close out the usability gap
@xander-abn surfaced in #4329:

1. Composite-projection cache eviction fix (JasperFx/jasperfx#206) —
   AppointmentByExternalIdentifier (and similar EnrichWith<UpstreamDoc>
   chains) no longer drop downstream lookups when the upstream's
   CacheLimitPerTenant is small relative to per-batch fan-out.

2. ForEntityIds (JasperFx/jasperfx#208) — first-class declarative shape for
   1-event-to-N-entities enrichment.

This commit:

- Bumps JasperFx.Events 1.34.0 → 1.35.0 in Directory.Packages.props.
- Adds Bug_4329_fan_out_and_cache_limit with two facts: a fan-out scenario
  using ForEntityIds (one OrderPlacedWithLineItems event referencing 5
  upstream Product documents), and a cache-eviction regression that
  intentionally sets ProductProjection.CacheLimitPerTenant = 1 and verifies
  20 distinct upstream lookups all resolve to the in-flight cache.
- Wires the new patterns into composite.md and enrichment.md, removes the
  now-stale "cache-limit is load-bearing for correctness" caveat, and
  adds a "Fan-out enrichment with ForEntityIds" section sourced from
  #region sample_for_entity_ids_fan_out in the new test.

Refs #4329, JasperFx/jasperfx#206, JasperFx/jasperfx#208.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant